Implementando arrastrar y soltar nativo en HTML5

Hoy vuelvo al blog con la traducción -no literal- del artículo "Implementing native drag and drop", publicada por Treehouse en su blog.

Haciendo que las cosas sea arrastrables


Para hacer que un elemento sea arrastrable en HTML5 sólo tenemos que añadir la etiqueta draggable=true.
<div draggable="true">Elemento div arrastrable</div>
Algunos elementos como los enlaces a y las imágenes img son arrastrables por defecto, aunque es mejor establecer la propiedad draggable por seguridad.

Escuchando eventos de arrastre

Hay unos cuantos eventos que tienen lugar durante una interacción de arrastre. Algunos de estos eventos se disparan desde el elemento que está siendo arrastrado, mientras que otros se disparan desde los elementos de la página que sirven como destino para soltar.
  • dragstart El evento se dispara en un elemento cuando empieza el arrastre. No se dispara cuando se arrastra un fichero al navegador desde el sistema de ficheros del equipo.
  • drag Este evento se dispara continuamente mientras el elemento es arrastrado durante la interacción.
  • dragenter El evento se dispara cuando el elemento arrastrado entra en el elemento de destino. El event listener debe especificarse en el elemento de destino.
  • dragleave Como el evento anterior, pero cuando el elemento sale del elemento de destino.
  • dragover Este evento se dispara continuamente mientras el elemento arrastrado pasa sobre el elemento de destino.
  • drop Este evento se dispara cuando se suelta el elemento arrastrado o el fichero.
  • dragend Este evento se dispara una vez cuando la interacción ha acabado. Se aplica al elemento que estaba siendo arrastrado.
Los eventos de ratón (mouse events) no se disparan durante una interacción de arrastrar y soltar.
Podemos usar un event listener para ejecutar código cuando cualquiera de estos eventos se dispara. Por ejemplo:
elementoArrastrable.addEventListener('dragstart', function(e){
   console.log('Arrastrando!');
});

El objeto DataTransfer

 Cuando una interacción de arrastre se inicia se crea el objeto DataTransfer asociado a la interacción. El objeto almacena información tanto sobre la interacción de arrastre como sobre la información relativa a los elementos implicados.

A continuación se indican algunas de las propiedades del objeto DataTransfer:

  • dropEffect El tipo de interacción de arrastrar y soltar. Determina el cursor que el navegador muestra durante la interacción. Los valores posibles son copy, move, link y none.
  • effectAllowed Especifica qué tipos son permitidos para esta interacción. Los valores posibles son copy, move, link, copyLink, copyMove, linkMove, all, none y unitialized (que es el valor por defecto y que se trata del mismo modo que all).
  • files Una fila FileList que contiene los objetos File asociados a la acción de arrastre.
  • setData(format, data) Este método se utiliza para almacenar información en el objeto DataTransfer. La cadena format contiene el formato de los datos que se almacenan (por ejemplo, text, url, text/html).
  • getData(format) Este método se utiliza para obtener información del objeto DataTransfer
  • clearData(format) Este método se utiliza para eliminar datos del objeto DataTransfer. Si se especifica el parámetro opcional format, sólo se elimina la información que coincide con el formato especificado. Si no se especifica ningún tipo, se elimina toda la información del objeto.
  • setDragImage(imgElement, x, y) Este método permite especificar una imagen personalizada para mostrar durante el arrastre. Hay que pasar un elemento img, no la ruta a una imagen así como las coordenadas que especifican la posición relativa al movimiento del cursor.
El objeto dataTransfer es accesible en el evento que se pasa al event listener definido. Por ejemplo: 
elementoArrastrable.addEventListener('dragstart', function(event){
   event.dataTransfer.setData('text', 'Hola mundo!');
);
En primer lugar escribimos HTML en el que incluimos algunos elementos arrastrables y un <div> que nos servirá como destino para soltarlos:
<ul id="elementos-arrastrables">
   <li draggable="true">Elemento 1</li>
   <li draggable="true">Elemento 2</li>
   <li draggable="true">Elemento 3</li>
</ul>

<div id="destino-soltar">
Soltar aquí
</div>
Una vez creado el fichero HTML creamos el fichero JavaScript y empezamos a escribir el código necesario para gestionar la interacción de arrastrar y soltar.

En primer lugar, se definen las variables que contienen referencias a los elementos DOM del documento:
// Seleccionamos el elemento de destino de la acción de soltar
var zonaSoltarUno = document.querySelector('#destino-soltar');

// Obtenemos los elementos arrastrables del documento
var elementosArrastrables = document.querySelector('#elementos-arrastrables li');

// Seguimos la pista del elemento arrastrado
var elementoArrastrado = null;
Lo siguiente es establecer event listeners para los eventos de inicio y fin del arrastre. Estos event listeners deben asociarse a cada uno de los elementos arrastrables, por lo que utilizamos un bucle para recorrer todos los elementos contenidos en la variable elementosArrastrables.

Cuando el evento dragStart se dispara establecemos el valor de la propiedad effectAllowed como move. También almacenamos el contenido del elemento arrastrado en el objeto dataTransfer. Finalmente, establecemos la variable elementoArrastrado con el valor del elemento que está siendo arrastrado.

Para el evento dragEnd sólo realizamos un poco de limpieza, estableciendo el elemento elementoArrastrado de nuevo como null.
for (var i = 0; i < elementosArrastrados.length; i++) {
   // Event listener para cuando empieza la interacción de arrastre.
   elementosArrastradps[i].addEventListener('dragstart', function(e) {
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text', this.innerHTML);
      elementoArrastrado = this;
   });
   // Event Listener para cuando la interacción de arrastre finaliza.
   elementosArrastrados[i].addEventListener('dragend', function(e) {
      elementoArrastrado = null;
   });
};
A continuación necesitamos crear un event listener que se dispare cuando el elemento sea arrastrado sobre el elemento de destino. Entonces estableceremos la propiedad dropEffect del objeto dataTransfer a move, haciendo que el navegador actualice el cursor mostrado.
// Event listener para cuando el elemento arrastrado está sobre la zona de destino
zonaSoltarUno.addEventListener('dragover', function(e) {
    if (e.preventDefault) {
        e.preventDefault();
    }

    e.dataTransfer.dropEffect = 'move';
    return false;
});
Ejecutar la función e.preventDefault() y e.stopPropagation() (usada más adelante) es necesario para evitar que el navegador ejecute cualquier comportamiento establecido por defecto para el elemento arrastrable, cosa que podría interferir con nuestra acción de arrastrar y soltar.

Es interesante cambiar el estilo de la zona de destino cuando el elemento arrastrado está sobre ella. Para conseguirlo, aplicamos la clase over al elemento de destino cuando se dispare el evento dragenter y la eliminaremos cuando se dispare el evento dragleave.

Usamos dragenter y dragleave en vez de dragover porque éste se dispara continuamente mientras nos desplazamos por el evento de destino. dragenter  y dragleave sólo se disparan una vez cada uno, al entrar y al salir del elemento de destino, con lo que conseguimos minimizar el trabajo del navegador.
zonaSoltarUno.addEventListerner('dragenter', function(e){
    this.className = 'over';
});
zonaSoltarUno.addEventListerner('dragleave' function(e){
    this.className = '';
});
Finalmente, tenemos que establecer un event listener para el evento de soltar (drop):
zonaSoltarUno.addEventListener('drop', function(e){
    if (e.preventDefault) e.preventDefault();
    if (e.stopPropagation) e.stopPropagation();
    this.className = "";
    this.innerHTML = 'Soltado " + e.dataTransfer.getData('text');
    document.querySelector('#elementos-arrastrables').removeChild(elementoArrastrado);
    return false;
});

Comentarios